package com.sneakxy.mybatis.commons.repository.support;

import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;

import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.type.TypeHandlerRegistry;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher.NoOpPropertyValueTransformer;
import org.springframework.data.domain.ExampleMatcher.NullHandler;
import org.springframework.data.domain.ExampleMatcher.PropertySpecifier;
import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer;
import org.springframework.data.domain.ExampleMatcher.StringMatcher;
import org.springframework.data.support.ExampleMatcherAccessor;

import com.sneakxy.mybatis.commons.orm.mapping.MybatisColumn;
import com.sneakxy.mybatis.commons.repository.query.domain.Criteria;
import com.sneakxy.mybatis.commons.repository.query.domain.CriteriaBuilder;
import com.sneakxy.mybatis.commons.repository.support.transformer.ContainingPropertyValueTransformer;
import com.sneakxy.mybatis.commons.repository.support.transformer.EndingPropertyValueTransformer;
import com.sneakxy.mybatis.commons.repository.support.transformer.StartingPropertyValueTransformer;
import com.sneakxy.mybatis.commons.utils.MybatisUtils;

/**
 * @author 潜行的青衣
 */
public class MybatisExampleAccessor<T> {

	private Example<T> example;
	
	private ExampleMatcherAccessor accessor;
	
	private TypeHandlerRegistry typeHandlerRegistry;
	
	private Collection<String> properties;
	
	public MybatisExampleAccessor(Example<T> example, TypeHandlerRegistry typeHandlerRegistry) {
		this(example, null, typeHandlerRegistry);
	}
	
	public MybatisExampleAccessor(Example<T> example, Collection<String> properties, TypeHandlerRegistry typeHandlerRegistry) {
		this.example = example;
		this.typeHandlerRegistry = typeHandlerRegistry;
		this.accessor = new ExampleMatcherAccessor(example.getMatcher());
		this.properties = properties;
	}
	
	public Criteria<T> toCriteria() throws IllegalAccessException {
		CriteriaBuilder<T> builder = new CriteriaBuilder<>(this.getExample().getProbeType());
		T obj = getExample().getProbe();
		List<MybatisColumn> columns = MybatisColumn.getMybatisColumns(getTypeHandlerRegistry(), getExample().getProbeType());
		Iterator<MybatisColumn> it = columns.iterator();
		Class<T> domainClass = this.getExample().getProbeType();
		CriteriaBuilderAccessor<T> criteriaBuilderAccessor = null;
		boolean isAny = getExample().getMatcher().isAnyMatching();
		boolean hasProperties = properties != null && !properties.isEmpty();
		boolean ingoreCase = accessor.isIgnoreCaseEnabled();
		boolean include = accessor.getNullHandler() == NullHandler.INCLUDE;
		PropertySpecifier specifier = null;
		MybatisColumn column = null;
		Object value = null;
		while(it.hasNext()) {
			column = it.next();
			if(isIgnoredPath(hasProperties, column.getPropertyPath())) {
				continue;
			}
			value = MybatisUtils.getFieldValue(domainClass, obj, column.getPropertyPath());
			if(value != null) {
				specifier = getPropertySpecifier(column.getPropertyPath());
				if(specifier != null) {
					criteriaBuilderAccessor = getCriteriaBuilderAccessor(specifier);
					criteriaBuilderAccessor.build(builder, isAny, column.getPropertyPath(), value, ingoreCase || (specifier.getIgnoreCase() != null && specifier.getIgnoreCase()));
				} else {
					criteriaBuilderAccessor = new EqualsCriteriaBuilderAccessor<>(NoOpPropertyValueTransformer.INSTANCE);
					criteriaBuilderAccessor.build(builder, isAny, column.getPropertyPath(), value, ingoreCase);
				}
			} else {
				if(include) {
					criteriaBuilderAccessor = new NullableCriteriaBuilderAccessor<>(NoOpPropertyValueTransformer.INSTANCE);
					criteriaBuilderAccessor.build(builder, isAny, column.getPropertyPath(), value, ingoreCase);
				}
			}
		}
		return builder.build();
	}
	
	protected CriteriaBuilderAccessor<T> getCriteriaBuilderAccessor(PropertySpecifier specifier) {
		PropertyValueTransformer transformer = specifier.getPropertyValueTransformer();
		CriteriaBuilderAccessor<T> criteriaBuilderAccessor = null;
		boolean isDefault = NoOpPropertyValueTransformer.INSTANCE == transformer;
		StringMatcher springMatcher = specifier.getStringMatcher();
		springMatcher = springMatcher == null ? StringMatcher.DEFAULT : springMatcher;
		switch(springMatcher) {
			case STARTING:
				transformer = isDefault ? new StartingPropertyValueTransformer() : transformer;
				criteriaBuilderAccessor = new LikeCriteriaBuilderAccessor<>(transformer);
				break;
			case ENDING:
				transformer = isDefault ? new EndingPropertyValueTransformer() : transformer;
				criteriaBuilderAccessor = new LikeCriteriaBuilderAccessor<>(transformer);
				break;
			case CONTAINING:
				transformer = isDefault ? new ContainingPropertyValueTransformer() : transformer;
				criteriaBuilderAccessor = new LikeCriteriaBuilderAccessor<>(transformer);
				break;
			case REGEX:
				criteriaBuilderAccessor = new RegexCriteriaBuilderAccessor<>(transformer);
				break;
			default:
				criteriaBuilderAccessor = new EqualsCriteriaBuilderAccessor<>(transformer);
		}
		return criteriaBuilderAccessor;
	}
	
	protected boolean isIgnoredPath(boolean hasProperties, String propertyPath) {
		if(hasProperties && !properties.contains(propertyPath)) {
			return true;
		}
		if(StringUtils.contains(propertyPath, ".")) {
			String[] props = StringUtils.split(propertyPath, "\\.");
			StringBuilder pathBuilder = new StringBuilder();
			for(String prop:props) {
				if(pathBuilder.length() > 0) {
					pathBuilder.append(".");
				}
				pathBuilder.append(prop);
				if(this.accessor.isIgnoredPath(pathBuilder.toString())) {
					return true;
				}
			}
		}
		return accessor.isIgnoredPath(propertyPath);
	}
	
	protected PropertySpecifier getPropertySpecifier(String propertyPath) {
		return accessor.getPropertySpecifier(propertyPath);
	}

	public TypeHandlerRegistry getTypeHandlerRegistry() {
		return typeHandlerRegistry;
	}

	public Example<T> getExample() {
		return example;
	}

	protected ExampleMatcherAccessor getAccessor() {
		return accessor;
	}
	
	public static class EqualsCriteriaBuilderAccessor<T> extends AbstractCriteriaBuilderAccessor<T> {

		/**
		 * @param propertyValueTransformer
		 */
		public EqualsCriteriaBuilderAccessor(PropertyValueTransformer propertyValueTransformer) {
			super(propertyValueTransformer);
		}

		/* (non-Javadoc)
		 * @see com.sneakxy.mybatis.commons.repository.support.MybatisExampleAccessor.CriteriaBuilderAccessor#onBuild(com.sneakxy.mybatis.commons.repository.query.domain.CriteriaBuilder)
		 */
		@Override
		public void build(CriteriaBuilder<T> builder, boolean isAny, String propertyPath, Object source, boolean ignoreCase) {
			if(isAny) {
				builder.orEq(propertyPath, getValue(source), ignoreCase);
			} else {
				builder.andEq(propertyPath, getValue(source), ignoreCase);
			}
		}
		
	}
	
	public static class NullableCriteriaBuilderAccessor<T> extends AbstractCriteriaBuilderAccessor<T> {

		/**
		 * @param propertyValueTransformer
		 */
		public NullableCriteriaBuilderAccessor(PropertyValueTransformer propertyValueTransformer) {
			super(propertyValueTransformer);
		}

		/* (non-Javadoc)
		 * @see com.sneakxy.mybatis.commons.repository.support.MybatisExampleAccessor.CriteriaBuilderAccessor#onBuild(com.sneakxy.mybatis.commons.repository.query.domain.CriteriaBuilder)
		 */
		@Override
		public void build(CriteriaBuilder<T> builder, boolean isAny, String propertyPath, Object source, boolean ignoreCase) {
			if(isAny) {
				builder.orIsNull(propertyPath);
			} else {
				builder.andIsNull(propertyPath);
			}
		}
	}
	
	public static class LikeCriteriaBuilderAccessor<T> extends AbstractCriteriaBuilderAccessor<T> {

		/**
		 * @param propertyValueTransformer
		 */
		public LikeCriteriaBuilderAccessor(PropertyValueTransformer propertyValueTransformer) {
			super(propertyValueTransformer);
		}

		/* (non-Javadoc)
		 * @see com.sneakxy.mybatis.commons.repository.support.MybatisExampleAccessor.CriteriaBuilderAccessor#onBuild(com.sneakxy.mybatis.commons.repository.query.domain.CriteriaBuilder)
		 */
		@Override
		public void build(CriteriaBuilder<T> builder, boolean isAny, String propertyPath, Object source, boolean ignoreCase) {
			if(isAny) {
				builder.orLike(propertyPath, getValue(source), ignoreCase);
			} else {
				builder.andLike(propertyPath, getValue(source), ignoreCase);
			}
		}
		
	}
	
	public static class RegexCriteriaBuilderAccessor<T> extends AbstractCriteriaBuilderAccessor<T> {

		/**
		 * @param propertyValueTransformer
		 */
		public RegexCriteriaBuilderAccessor(PropertyValueTransformer propertyValueTransformer) {
			super(propertyValueTransformer);
		}

		/* (non-Javadoc)
		 * @see com.sneakxy.mybatis.commons.repository.support.MybatisExampleAccessor.CriteriaBuilderAccessor#onBuild(com.sneakxy.mybatis.commons.repository.query.domain.CriteriaBuilder)
		 */
		@Override
		public void build(CriteriaBuilder<T> builder, boolean isAny, String propertyPath, Object source, boolean ignoreCase) {
			if(isAny) {
				builder.orRegex(propertyPath, getValue(source));
			} else {
				builder.andRegex(propertyPath, getValue(source));
			}
		}
		
	}
	
	public static interface CriteriaBuilderAccessor<T> {
		
		public void build(CriteriaBuilder<T> builder, boolean isAny, String propertyPath, Object source, boolean ignoreCase);
		
	}
	
	public static abstract class AbstractCriteriaBuilderAccessor<T> implements CriteriaBuilderAccessor<T> {

		private PropertyValueTransformer propertyValueTransformer;
		
		public AbstractCriteriaBuilderAccessor(PropertyValueTransformer propertyValueTransformer) {
			this.propertyValueTransformer = propertyValueTransformer;
		}

		public PropertyValueTransformer getPropertyValueTransformer() {
			return propertyValueTransformer;
		}
		
		public Object getValue(Object source) {
			return this.getPropertyValueTransformer().apply(Optional.of(source)).get();
		}
		
	}
	
}
